Decorators

Decorators are a way to do aspect oriented programming in Python. Decorators were added in Python 2.4 and many libraries make extensive use of them.

In the simplest form

@dec
def foo():
    pass

is equivalent to

def foo():
    pass
foo = dec(foo)

For the above you can see that a decorator is a function that takes a function as an argument and returns a function.

Notes

Since their introduction in Python 2.4, decorators are everywhere. Including some in the standard library: property, staticmethod and others. Many libraries also use decorators: Celery @task, Flask @app.route and many more.

Decorators can be nested

@logged
@metrics.ncalls
@retry(times=3)
def whos_on_first():
    return 'who'


There are also class decorators (added in Python 2.6). You will probably won't need them (unless you wrote metaclasses before).

@plugin
class FunnyPlugin(object):
    pass

Exercise: Greeter

Create a decorator that prints 'Hello FUNCTION_NAME' before the function start and 'By FUNCTIONNAME' after the function finishes. (You can can the function name from the `__name__` attribute).


In [2]:
from functools import wraps
def greeter(fn):
    @wraps(fn)
    def wrapper(*args, **kw):
        print('Hello {}'.format(fn.__name__))
        try:
            return fn(*args, **kw)
        finally:
            print('Bye {}'.format(fn.__name__))

    return wrapper

In [3]:
@greeter
def add(x, y):
    '''Adds x to y'''
    return x + y

print(add(1, 2))


Hello add
Bye add
3

Exercise: Timer

Write a timed decorators that logs timing information for a given function.


In [18]:
from time import time

def timed(fn):
    @wraps(fn)
    def wrapper(*args, **kw):
        start = time()
        try:
            return fn(*args, **kw)
        finally:
            duration = time() - start
            print('{} took ({:.2f}sec)'.format(fn.__name__, duration))

    return wrapper

In [19]:
from time import sleep
@timed
def mul(x, y):
    '''Multiply x with y'''
    sleep(0.2)
    return x * y

mul(8, 4)
# return 32 and print timing info


mul took (0.20sec)
Out[19]:
32

Bonus: Use timed_block

Rewrite your decorator using timed_block context manager.


In [8]:
from ctx import timed_block
def timed(fn):
    @wraps(fn)
    def wrapper(*args, **kw):
        with timed_block(fn.__name__):
            return fn(*args, **kw)
    return wrapper

Exercise: Caching

Write a decorator that caches the results of the invoked function. Computing every value only once. (See lru_cache in Python > 3.2).


In [1]:
def cached(fn):
    cache = {}  # In scope for wrapper
    
    def wrapper(*args, **kw):
        if args not in cache:   # Ignore **kw for simplicity
            cache[args] = fn(*args, **kw)
        return cache[args]
    
    return wrapper

In [2]:
@cached
def fib(n):
    '''Return the n'th fibonacci number'''
    print('fib({})'.format(n))
    if n < 2:
        return 1
    return fib(n-1) + fib(n-2)

fib(5)


fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(0)
Out[2]:
8

Exercise: Multiplier

Write a decorator the multiplies the result of its wrapped function by n, where n is a parameter to the decorator.


In [27]:
def mulby(n):
    def wrapper(fn):
        @wraps(fn)
        def wrapped(*args, **kw):
            return n * fn(*args, **kw)

        return wrapped
    return wrapper

In [28]:
@mulby(7)
def inc(x):
    '''Add 1 to x'''
    return x + 1

inc(2)  # 21


Out[28]:
21

@property decorator

property makes a function looks like an ordinary attribute. It let's you change your mind and have more controlled access to attribute (say locking) without any change to the client.


In [4]:
class Parrot(object):
    def __init__(self, voltage):
        self._voltage = voltage
        
    @property
    def voltage(self):
        return self._voltage
    
p = Parrot(10)
print(p.voltage)  # Note: Not a function call


10

In [ ]: